// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.content.browser;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.Log;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.ViewConfiguration;
import org.chromium.base.CommandLine;
import org.chromium.base.TraceEvent;
import org.chromium.content.browser.third_party.GestureDetector;
import org.chromium.content.browser.third_party.GestureDetector.OnDoubleTapListener;
import org.chromium.content.browser.third_party.GestureDetector.OnGestureListener;
import org.chromium.content.common.ContentSwitches;
/**
* This class handles all MotionEvent handling done in ContentViewCore including the gesture
* recognition. It sends all related native calls through the interface MotionEventDelegate.
*/
class ContentViewGestureHandler {
private static final String TAG = "ContentViewGestureHandler";
/**
* Used for FLING_START x velocity
*/
static final String VELOCITY_X = "Velocity X";
/**
* Used for FLING_START y velocity
*/
static final String VELOCITY_Y = "Velocity Y";
/**
* Used for SCROLL_BY x distance (scroll offset of update)
*/
static final String DISTANCE_X = "Distance X";
/**
* Used for SCROLL_BY y distance (scroll offset of update)
*/
static final String DISTANCE_Y = "Distance Y";
/**
* Used for SCROLL_START delta X hint (movement triggering scroll)
*/
static final String DELTA_HINT_X = "Delta Hint X";
/**
* Used for SCROLL_START delta Y hint (movement triggering scroll)
*/
static final String DELTA_HINT_Y = "Delta Hint Y";
/**
* Used in SINGLE_TAP_CONFIRMED to check whether ShowPress has been called before.
*/
static final String SHOW_PRESS = "ShowPress";
/**
* Used for PINCH_BY delta
*/
static final String DELTA = "Delta";
private final Bundle mExtraParamBundleSingleTap;
private final Bundle mExtraParamBundleFling;
private final Bundle mExtraParamBundleScroll;
private final Bundle mExtraParamBundleScrollStart;
private final Bundle mExtraParamBundleDoubleTapDragZoom;
private final Bundle mExtraParamBundlePinchBy;
private GestureDetector mGestureDetector;
private OnGestureListener mListener;
private OnDoubleTapListener mDoubleTapListener;
private ScaleGestureDetector mMultiTouchDetector;
private ScaleGestureListener mMultiTouchListener;
private MotionEvent mCurrentDownEvent;
private final MotionEventDelegate mMotionEventDelegate;
// Remember whether onShowPress() is called. If it is not, in onSingleTapConfirmed()
// we will first show the press state, then trigger the click.
private boolean mShowPressIsCalled;
// Whether a sent TAP_DOWN event has yet to be accompanied by a corresponding
// SINGLE_TAP_UP, SINGLE_TAP_CONFIRMED, TAP_CANCEL or DOUBLE_TAP.
private boolean mNeedsTapEndingEvent;
// This flag is used for ignoring the remaining touch events, i.e., All the events until the
// next ACTION_DOWN. This is automatically set to false on the next ACTION_DOWN.
private boolean mIgnoreRemainingTouchEvents;
// TODO(klobag): this is to avoid a bug in GestureDetector. With multi-touch,
// mAlwaysInTapRegion is not reset. So when the last finger is up, onSingleTapUp()
// will be mistakenly fired.
private boolean mIgnoreSingleTap;
// True from right before we send the first scroll event until the last finger is raised.
private boolean mTouchScrolling;
// Used to remove the touch slop from the initial scroll event in a scroll gesture.
private boolean mSeenFirstScrollEvent;
private boolean mPinchInProgress = false;
private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();
// Indicate current double tap mode state.
private int mDoubleTapMode = DOUBLE_TAP_MODE_NONE;
// x, y coordinates for an Anchor on double tap drag zoom.
private float mDoubleTapDragZoomAnchorX;
private float mDoubleTapDragZoomAnchorY;
// On double tap this will store the y coordinates of the touch.
private float mDoubleTapY;
// Double tap drag zoom sensitive (speed).
private static final float DOUBLE_TAP_DRAG_ZOOM_SPEED = 0.005f;
// Used to track the last rawX/Y coordinates for moves. This gives absolute scroll distance.
// Useful for full screen tracking.
private float mLastRawX = 0;
private float mLastRawY = 0;
// Cache of square of the scaled touch slop so we don't have to calculate it on every touch.
private int mScaledTouchSlopSquare;
// Object that keeps track of and updates scroll snapping behavior.
private final SnapScrollController mSnapScrollController;
// Used to track the accumulated scroll error over time. This is used to remove the
// rounding error we introduced by passing integers to webkit.
private float mAccumulatedScrollErrorX = 0;
private float mAccumulatedScrollErrorY = 0;
// The page's viewport and scale sometimes allow us to disable double tap gesture detection,
// according to the logic in ContentViewCore.onRenderCoordinatesUpdated().
private boolean mShouldDisableDoubleTap;
// Keeps track of the last long press event, if we end up opening a context menu, we would need
// to potentially use the event to send TAP_CANCEL to remove ::active styling
private MotionEvent mLastLongPressEvent;
// Whether the click delay should always be disabled by sending clicks for double tap gestures.
private final boolean mDisableClickDelay;
private final float mPxToDp;
static final int DOUBLE_TAP_MODE_NONE = 0;
static final int DOUBLE_TAP_MODE_DRAG_DETECTION_IN_PROGRESS = 1;
static final int DOUBLE_TAP_MODE_DRAG_ZOOM = 2;
static final int DOUBLE_TAP_MODE_DISABLED = 3;
/**
* This is an interface to handle MotionEvent related communication with the native side also
* access some ContentView specific parameters.
*/
public interface MotionEventDelegate {
/**
* Signal the start of gesture detection for the provided {@link MotionEvent}.
* @param event The {@link MotionEvent} being fed to the gesture detectors.
*/
public void onTouchEventHandlingBegin(MotionEvent event);
/**
* Signal that all gestures for the current {@link MotionEvent} have been dispatched.
*/
public void onTouchEventHandlingEnd();
/**
* Forward a generated event to the client. This will normally be wrapped by
* calls to {@link #onTouchEventHandlingBegin(MotionEvent)} and
* {@link #onTouchEventHandlingEnd()}, unless the gesture is generated from
* a touch timeout, e.g., LONG_PRESS.
* @param type The type of the gesture event.
* @param timeMs The time the gesture event occurred at.
* @param x The x location for the gesture event.
* @param y The y location for the gesture event.
* @param extraParams A bundle that holds specific extra parameters for certain gestures.
* This is read-only and should not be modified in this function.
* Refer to gesture type definition for more information.
* @return Whether the gesture was forwarded successfully.
*/
boolean onGestureEventCreated(int type, long timeMs, int x, int y, Bundle extraParams);
}
ContentViewGestureHandler(Context context, MotionEventDelegate delegate) {
mExtraParamBundleSingleTap = new Bundle();
mExtraParamBundleFling = new Bundle();
mExtraParamBundleScroll = new Bundle();
mExtraParamBundleScrollStart = new Bundle();
mExtraParamBundleDoubleTapDragZoom = new Bundle();
mExtraParamBundlePinchBy = new Bundle();
mMotionEventDelegate = delegate;
mSnapScrollController = new SnapScrollController(context);
mPxToDp = 1.0f / context.getResources().getDisplayMetrics().density;
mDisableClickDelay = CommandLine.isInitialized() &&
CommandLine.getInstance().hasSwitch(ContentSwitches.DISABLE_CLICK_DELAY);
initGestureDetectors(context);
}
private void initGestureDetectors(final Context context) {
final int scaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mScaledTouchSlopSquare = scaledTouchSlop * scaledTouchSlop;
try {
TraceEvent.begin();
GestureDetector.SimpleOnGestureListener listener =
new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
mShowPressIsCalled = false;
mIgnoreSingleTap = false;
mTouchScrolling = false;
mSeenFirstScrollEvent = false;
mLastRawX = e.getRawX();
mLastRawY = e.getRawY();
mAccumulatedScrollErrorX = 0;
mAccumulatedScrollErrorY = 0;
mLastLongPressEvent = null;
mNeedsTapEndingEvent = false;
if (sendMotionEventAsGesture(GestureEventType.TAP_DOWN, e, null)) {
mNeedsTapEndingEvent = true;
}
// Return true to indicate that we want to handle touch
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float rawDistanceX, float rawDistanceY) {
assert e1.getEventTime() <= e2.getEventTime();
float distanceX = rawDistanceX;
float distanceY = rawDistanceY;
if (!mSeenFirstScrollEvent) {
// Remove the touch slop region from the first scroll event to avoid a
// jump.
mSeenFirstScrollEvent = true;
double distance = Math.sqrt(
distanceX * distanceX + distanceY * distanceY);
double epsilon = 1e-3;
if (distance > epsilon) {
double ratio = Math.max(0, distance - scaledTouchSlop) / distance;
distanceX *= ratio;
distanceY *= ratio;
}
}
mSnapScrollController.updateSnapScrollMode(distanceX, distanceY);
if (mSnapScrollController.isSnappingScrolls()) {
if (mSnapScrollController.isSnapHorizontal()) {
distanceY = 0;
} else {
distanceX = 0;
}
}
mLastRawX = e2.getRawX();
mLastRawY = e2.getRawY();
if (!mTouchScrolling) {
sendTapCancelIfNecessary(e1);
// Note that scroll start hints are in distance traveled, where
// scroll deltas are in the opposite direction.
mExtraParamBundleScrollStart.putInt(DELTA_HINT_X, (int) -rawDistanceX);
mExtraParamBundleScrollStart.putInt(DELTA_HINT_Y, (int) -rawDistanceY);
assert mExtraParamBundleScrollStart.size() == 2;
if (sendGesture(GestureEventType.SCROLL_START, e2.getEventTime(),
(int) e1.getX(), (int) e1.getY(),
mExtraParamBundleScrollStart)) {
mTouchScrolling = true;
}
}
// distanceX and distanceY is the scrolling offset since last onScroll.
// Because we are passing integers to webkit, this could introduce
// rounding errors. The rounding errors will accumulate overtime.
// To solve this, we should be adding back the rounding errors each time
// when we calculate the new offset.
int x = (int) e2.getX();
int y = (int) e2.getY();
int dx = (int) (distanceX + mAccumulatedScrollErrorX);
int dy = (int) (distanceY + mAccumulatedScrollErrorY);
mAccumulatedScrollErrorX = distanceX + mAccumulatedScrollErrorX - dx;
mAccumulatedScrollErrorY = distanceY + mAccumulatedScrollErrorY - dy;
mExtraParamBundleScroll.putInt(DISTANCE_X, dx);
mExtraParamBundleScroll.putInt(DISTANCE_Y, dy);
assert mExtraParamBundleScroll.size() == 2;
if ((dx | dy) != 0) {
sendGesture(GestureEventType.SCROLL_BY,
e2.getEventTime(), x, y, mExtraParamBundleScroll);
}
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2,
float velocityX, float velocityY) {
assert e1.getEventTime() <= e2.getEventTime();
if (mSnapScrollController.isSnappingScrolls()) {
if (mSnapScrollController.isSnapHorizontal()) {
velocityY = 0;
} else {
velocityX = 0;
}
}
fling(e2.getEventTime(), (int) e1.getX(0), (int) e1.getY(0),
(int) velocityX, (int) velocityY);
return true;
}
@Override
public void onShowPress(MotionEvent e) {
mShowPressIsCalled = true;
sendMotionEventAsGesture(GestureEventType.SHOW_PRESS, e, null);
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
if (isDistanceBetweenDownAndUpTooLong(e.getRawX(), e.getRawY())) {
sendTapCancelIfNecessary(e);
mIgnoreSingleTap = true;
return true;
}
// This is a hack to address the issue where user hovers
// over a link for longer than DOUBLE_TAP_TIMEOUT, then
// onSingleTapConfirmed() is not triggered. But we still
// want to trigger the tap event at UP. So we override
// onSingleTapUp() in this case. This assumes singleTapUp
// gets always called before singleTapConfirmed.
if (!mIgnoreSingleTap) {
if (e.getEventTime() - e.getDownTime() > DOUBLE_TAP_TIMEOUT) {
if (sendTapEndingEventAsGesture(
GestureEventType.SINGLE_TAP_UP, e, null)) {
mIgnoreSingleTap = true;
}
return true;
} else if (isDoubleTapDisabled() || mDisableClickDelay) {
// If double tap has been disabled, there is no need to wait
// for the double tap timeout.
return onSingleTapConfirmed(e);
} else {
// Notify Blink about this tapUp event anyway,
// when none of the above conditions applied.
sendMotionEventAsGesture(
GestureEventType.SINGLE_TAP_UNCONFIRMED, e, null);
}
}
return triggerLongTapIfNeeded(e);
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
// Long taps in the edges of the screen have their events delayed by
// ContentViewHolder for tab swipe operations. As a consequence of the delay
// this method might be called after receiving the up event.
// These corner cases should be ignored.
if (mIgnoreSingleTap) return true;
mExtraParamBundleSingleTap.putBoolean(SHOW_PRESS, mShowPressIsCalled);
assert mExtraParamBundleSingleTap.size() == 1;
if (sendTapEndingEventAsGesture(GestureEventType.SINGLE_TAP_CONFIRMED, e,
mExtraParamBundleSingleTap)) {
mIgnoreSingleTap = true;
}
return true;
}
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
switch (e.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
// Note that this will be called before the corresponding |onDown()|
// of the same ACTION_DOWN event. Thus, the preceding TAP_DOWN
// should be cancelled prior to sending a new one (in |onDown()|).
sendTapCancelIfNecessary(e);
mDoubleTapDragZoomAnchorX = e.getX();
mDoubleTapDragZoomAnchorY = e.getY();
mDoubleTapMode = DOUBLE_TAP_MODE_DRAG_DETECTION_IN_PROGRESS;
// If a long-press fires during a double-tap, the GestureDetector
// will stop feeding MotionEvents to |onDoubleTapEvent()|,
// preventing double-tap drag zoom. Long press detection will be
// re-enabled on the next ACTION_DOWN.
mGestureDetector.setIsLongpressEnabled(false);
break;
case MotionEvent.ACTION_MOVE:
if (mDoubleTapMode
== DOUBLE_TAP_MODE_DRAG_DETECTION_IN_PROGRESS) {
float distanceX = mDoubleTapDragZoomAnchorX - e.getX();
float distanceY = mDoubleTapDragZoomAnchorY - e.getY();
// Begin double tap drag zoom mode if the move distance is
// further than the threshold.
if (isDistanceGreaterThanTouchSlop(distanceX, distanceY)) {
sendTapCancelIfNecessary(e);
mExtraParamBundleScrollStart.putInt(DELTA_HINT_X,
(int) -distanceX);
mExtraParamBundleScrollStart.putInt(DELTA_HINT_Y,
(int) -distanceY);
assert mExtraParamBundleScrollStart.size() == 2;
sendGesture(GestureEventType.SCROLL_START, e.getEventTime(),
(int) e.getX(), (int) e.getY(),
mExtraParamBundleScrollStart);
pinchBegin(e.getEventTime(),
Math.round(mDoubleTapDragZoomAnchorX),
Math.round(mDoubleTapDragZoomAnchorY));
mDoubleTapMode = DOUBLE_TAP_MODE_DRAG_ZOOM;
}
} else if (mDoubleTapMode == DOUBLE_TAP_MODE_DRAG_ZOOM) {
assert mExtraParamBundleDoubleTapDragZoom.isEmpty();
sendGesture(GestureEventType.SCROLL_BY, e.getEventTime(),
(int) e.getX(), (int) e.getY(),
mExtraParamBundleDoubleTapDragZoom);
float dy = mDoubleTapY - e.getY();
pinchBy(e.getEventTime(),
Math.round(mDoubleTapDragZoomAnchorX),
Math.round(mDoubleTapDragZoomAnchorY),
(float) Math.pow(dy > 0 ?
1.0f - DOUBLE_TAP_DRAG_ZOOM_SPEED :
1.0f + DOUBLE_TAP_DRAG_ZOOM_SPEED,
Math.abs(dy * mPxToDp)));
}
break;
case MotionEvent.ACTION_UP:
if (mDoubleTapMode != DOUBLE_TAP_MODE_DRAG_ZOOM) {
// Normal double tap gesture.
sendTapEndingEventAsGesture(
GestureEventType.DOUBLE_TAP, e, null);
}
endDoubleTapDragIfNecessary(e);
break;
case MotionEvent.ACTION_CANCEL:
sendTapCancelIfNecessary(e);
endDoubleTapDragIfNecessary(e);
break;
default:
break;
}
mDoubleTapY = e.getY();
return true;
}
@Override
public boolean onLongPress(MotionEvent e) {
assert !isDoubleTapActive();
if (isScaleGestureDetectionInProgress()) return false;
setIgnoreSingleTap(true);
mLastLongPressEvent = e;
sendMotionEventAsGesture(GestureEventType.LONG_PRESS, e, null);
// Returning true puts the GestureDetector in "longpress" mode, disabling
// further scrolling. This is undesirable, as it is quite common for a
// longpress gesture to fire on content that won't trigger a context menu.
return false;
}
/**
* This method inspects the distance between where the user started touching
* the surface, and where she released. If the points are too far apart, we
* should assume that the web page has consumed the scroll-events in-between,
* and as such, this should not be considered a single-tap.
*
* We use the Android frameworks notion of how far a touch can wander before
* we think the user is scrolling.
*
* @param x the new x coordinate
* @param y the new y coordinate
* @return true if the distance is too long to be considered a single tap
*/
private boolean isDistanceBetweenDownAndUpTooLong(float x, float y) {
return isDistanceGreaterThanTouchSlop(mLastRawX - x, mLastRawY - y);
}
};
mListener = listener;
mDoubleTapListener = listener;
mGestureDetector = new GestureDetector(context, listener);
mMultiTouchListener = new ScaleGestureListener();
mMultiTouchDetector = new ScaleGestureDetector(context, mMultiTouchListener);
// ScaleGestureDetector's "QuickScale" feature was introduced in KitKat.
// As ContentViewGestureHandler already implements this feature,
// explicitly disable it to prevent double-handling of the gesture.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
disableQuickScale(mMultiTouchDetector);
}
} finally {
TraceEvent.end();
}
}
private class ScaleGestureListener implements ScaleGestureDetector.OnScaleGestureListener {
// Completely silence scaling events. Used in WebView when zoom support
// is turned off.
private boolean mPermanentlyIgnoreDetectorEvents = false;
// Whether any pinch zoom event has been sent to native.
private boolean mPinchEventSent;
// ScaleGestureDetector previous to 4.2.2 failed to record the touch event time
// (b/7626515), so we record it manually for synthesizing pinch gestures.
private long mCurrentEventTime;
void setCurrentEventTime(long currentEventTime) {
mCurrentEventTime = currentEventTime;
}
private long getEventTime(ScaleGestureDetector detector) {
// Workaround for b/7626515, fixed in 4.2.2.
assert mCurrentEventTime != 0;
assert detector.getEventTime() == 0 || detector.getEventTime() == mCurrentEventTime;
return mCurrentEventTime;
}
boolean getPermanentlyIgnoreDetectorEvents() {
return mPermanentlyIgnoreDetectorEvents;
}
void setPermanentlyIgnoreDetectorEvents(boolean value) {
// Note that returning false from onScaleBegin / onScale makes the
// gesture detector not to emit further scaling notifications
// related to this gesture. Thus, if detector events are enabled in
// the middle of the gesture, we don't need to do anything.
mPermanentlyIgnoreDetectorEvents = value;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
if (mPermanentlyIgnoreDetectorEvents) return false;
mPinchEventSent = false;
setIgnoreSingleTap(true);
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
if (!mPinchEventSent) return;
pinchEnd(getEventTime(detector));
mPinchEventSent = false;
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
if (mPermanentlyIgnoreDetectorEvents) return false;
// It is possible that pinchBegin() was never called when we reach here.
// This happens when webkit handles the 2nd touch down event. That causes
// ContentView to ignore the onScaleBegin() call. And if webkit does not
// handle the touch move events afterwards, we will face a situation
// that pinchBy() is called without any pinchBegin().
// To solve this problem, we call pinchBegin() here if it is never called.
if (!mPinchEventSent) {
pinchBegin(getEventTime(detector),
(int) detector.getFocusX(), (int) detector.getFocusY());
mPinchEventSent = true;
}
pinchBy(getEventTime(detector), (int) detector.getFocusX(), (int) detector.getFocusY(),
detector.getScaleFactor());
return true;
}
};
@TargetApi(Build.VERSION_CODES.KITKAT)
private static void disableQuickScale(ScaleGestureDetector scaleGestureDetector) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return;
scaleGestureDetector.setQuickScaleEnabled(false);
}
/**
* Fling the ContentView from the current position.
* @param x Fling touch starting position
* @param y Fling touch starting position
* @param velocityX Initial velocity of the fling (X) measured in pixels per second.
* @param velocityY Initial velocity of the fling (Y) measured in pixels per second.
*/
void fling(long timeMs, int x, int y, int velocityX, int velocityY) {
if (velocityX == 0 && velocityY == 0) {
endTouchScrollIfNecessary(timeMs, true);
return;
}
if (!mTouchScrolling) {
// The native side needs a SCROLL_BEGIN before FLING_START
// to send the fling to the correct target. Send if it has not sent.
// The distance traveled in one second is a reasonable scroll start hint.
mExtraParamBundleScrollStart.putInt(DELTA_HINT_X, velocityX);
mExtraParamBundleScrollStart.putInt(DELTA_HINT_Y, velocityY);
assert mExtraParamBundleScrollStart.size() == 2;
sendGesture(GestureEventType.SCROLL_START, timeMs, x, y, mExtraParamBundleScrollStart);
}
endTouchScrollIfNecessary(timeMs, false);
mExtraParamBundleFling.putInt(VELOCITY_X, velocityX);
mExtraParamBundleFling.putInt(VELOCITY_Y, velocityY);
assert mExtraParamBundleFling.size() == 2;
sendGesture(GestureEventType.FLING_START, timeMs, x, y, mExtraParamBundleFling);
}
/**π
* End DOUBLE_TAP_MODE_DRAG_ZOOM by sending SCROLL_END and PINCH_END events.
* @param event A hint event that its x, y, and eventTime will be used for the ending events
* to send. This argument is an optional and can be null.
*/
private void endDoubleTapDragIfNecessary(MotionEvent event) {
assert event != null;
if (!isDoubleTapActive()) return;
if (mDoubleTapMode == DOUBLE_TAP_MODE_DRAG_ZOOM) {
pinchEnd(event.getEventTime());
sendGesture(GestureEventType.SCROLL_END, event.getEventTime(),
(int) event.getX(), (int) event.getY(), null);
}
mDoubleTapMode = DOUBLE_TAP_MODE_NONE;
updateDoubleTapListener();
}
/**
* Reset touch scroll flag and optionally send a SCROLL_END event if necessary.
* @param timeMs The time in ms for the event initiating this gesture.
* @param sendScrollEndEvent Whether to send SCROLL_END event.
*/
private void endTouchScrollIfNecessary(long timeMs, boolean sendScrollEndEvent) {
if (!mTouchScrolling) return;
mTouchScrolling = false;
if (sendScrollEndEvent) {
sendGesture(GestureEventType.SCROLL_END, timeMs, 0, 0, null);
}
}
/**
* @return Whether native is tracking a scroll.
*/
boolean isNativeScrolling() {
// TODO(wangxianzhu): Also return true when fling is active once the UI knows exactly when
// the fling ends.
return mTouchScrolling;
}
/**
* @return Whether native is tracking a pinch (i.e. between sending PINCH_BEGIN and PINCH_END).
*/
boolean isNativePinching() {
return mPinchInProgress;
}
/**
* Starts a pinch gesture.
* @param timeMs The time in ms for the event initiating this gesture.
* @param x The x coordinate for the event initiating this gesture.
* @param y The x coordinate for the event initiating this gesture.
*/
private void pinchBegin(long timeMs, int x, int y) {
sendGesture(GestureEventType.PINCH_BEGIN, timeMs, x, y, null);
}
/**
* Pinch by a given percentage.
* @param timeMs The time in ms for the event initiating this gesture.
* @param anchorX The x coordinate for the anchor point to be used in pinch.
* @param anchorY The y coordinate for the anchor point to be used in pinch.
* @param delta The percentage to pinch by.
*/
private void pinchBy(long timeMs, int anchorX, int anchorY, float delta) {
mExtraParamBundlePinchBy.putFloat(DELTA, delta);
assert mExtraParamBundlePinchBy.size() == 1;
sendGesture(GestureEventType.PINCH_BY, timeMs, anchorX, anchorY, mExtraParamBundlePinchBy);
mPinchInProgress = true;
}
/**
* End a pinch gesture.
* @param timeMs The time in ms for the event initiating this gesture.
*/
private void pinchEnd(long timeMs) {
sendGesture(GestureEventType.PINCH_END, timeMs, 0, 0, null);
mPinchInProgress = false;
}
/**
* Ignore singleTap gestures.
*/
void setIgnoreSingleTap(boolean value) {
mIgnoreSingleTap = value;
}
/**
* Cancel the current touch event sequence by sending ACTION_CANCEL and ignore all the
* subsequent events until the next ACTION_DOWN.
*
* One example usecase is stop processing the touch events when showing context popup menu.
*/
public void setIgnoreRemainingTouchEvents() {
if (mIgnoreRemainingTouchEvents) return;
MotionEvent me = obtainActionCancelMotionEvent();
if (mCurrentDownEvent != null) {
// Only insert a synthetic event if there's an active touch sequence.
onTouchEvent(me);
} else {
// Otherwise, we still want to reset the gesture detector pipeline
// (e.g., reset double-tap detection state).
mGestureDetector.onTouchEvent(me);
processTouchEventForMultiTouch(me);
}
me.recycle();
assert mCurrentDownEvent == null;
mIgnoreRemainingTouchEvents = true;
}
/**
* Handle the incoming MotionEvent.
* @return Whether the event was handled.
*/
boolean onTouchEvent(MotionEvent event) {
final int eventAction = event.getActionMasked();
// Only these actions have any effect on gesture detection. Other
// actions have no corresponding WebTouchEvent type and may confuse the
// touch pipline, so we ignore them entirely.
if (eventAction != MotionEvent.ACTION_DOWN
&& eventAction != MotionEvent.ACTION_UP
&& eventAction != MotionEvent.ACTION_CANCEL
&& eventAction != MotionEvent.ACTION_MOVE
&& eventAction != MotionEvent.ACTION_POINTER_DOWN
&& eventAction != MotionEvent.ACTION_POINTER_UP) {
return false;
}
try {
TraceEvent.begin("onTouchEvent");
if (mIgnoreRemainingTouchEvents) {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
mIgnoreRemainingTouchEvents = false;
} else {
return false;
}
}
return processTouchEvent(event);
} finally {
TraceEvent.end("onTouchEvent");
}
}
/**
* Handle content view losing focus -- ensure that any remaining active state is removed.
*/
void onWindowFocusLost() {
// TODO(jdduke): Determine if this should behave more like setIgnoreRemainingTouchEvents().
if (mLastLongPressEvent != null) {
sendTapCancelIfNecessary(mLastLongPressEvent);
}
}
private MotionEvent obtainActionCancelMotionEvent() {
MotionEvent me = MotionEvent.obtain(
mCurrentDownEvent != null ?
mCurrentDownEvent.getDownTime() : SystemClock.uptimeMillis(),
SystemClock.uptimeMillis(),
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
me.setSource(mCurrentDownEvent != null ?
mCurrentDownEvent.getSource() : InputDevice.SOURCE_CLASS_POINTER);
return me;
}
/**
* Resets gesture handlers state; called on didStartLoading().
* Note that this does NOT clear the pending motion events queue;
* it gets cleared in hasTouchEventHandlers() called from WebKit
* FrameLoader::transitionToCommitted iff the page ever had touch handlers.
*/
void resetGestureHandlers() {
MotionEvent me = obtainActionCancelMotionEvent();
mGestureDetector.onTouchEvent(me);
processTouchEventForMultiTouch(me);
me.recycle();
}
private boolean processTouchEvent(MotionEvent event) {
if (!canHandle(event)) return false;
try {
mMotionEventDelegate.onTouchEventHandlingBegin(event);
final boolean wasTouchScrolling = mTouchScrolling;
mSnapScrollController.setSnapScrollingMode(event, isScaleGestureDetectionInProgress());
if (event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
endDoubleTapDragIfNecessary(event);
} else if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
mGestureDetector.setIsLongpressEnabled(true);
mCurrentDownEvent = MotionEvent.obtain(event);
}
boolean handled = mGestureDetector.onTouchEvent(event);
handled |= processTouchEventForMultiTouch(event);
if (event.getAction() == MotionEvent.ACTION_UP
|| event.getAction() == MotionEvent.ACTION_CANCEL) {
if (event.getAction() == MotionEvent.ACTION_CANCEL) {
sendTapCancelIfNecessary(event);
}
// "Last finger raised" could be an end to movement, but it should
// only terminate scrolling if the event did not cause a fling.
if (wasTouchScrolling && !handled) {
endTouchScrollIfNecessary(event.getEventTime(), true);
}
if (mCurrentDownEvent != null) recycleEvent(mCurrentDownEvent);
mCurrentDownEvent = null;
}
return handled;
} finally {
mMotionEventDelegate.onTouchEventHandlingEnd();
}
}
private boolean isScaleGestureDetectionInProgress() {
return !mMultiTouchListener.getPermanentlyIgnoreDetectorEvents()
&& mMultiTouchDetector.isInProgress();
}
private boolean processTouchEventForMultiTouch(MotionEvent event) {
// TODO(jdduke): Need to deal with multi-touch transition
mMultiTouchListener.setCurrentEventTime(event.getEventTime());
try {
boolean inGesture = isScaleGestureDetectionInProgress();
boolean retVal = mMultiTouchDetector.onTouchEvent(event);
if (!inGesture && (event.getActionMasked() == MotionEvent.ACTION_UP
|| event.getActionMasked() == MotionEvent.ACTION_CANCEL)) {
return false;
}
return retVal;
} catch (Exception e) {
Log.e(TAG, "ScaleGestureDetector got into a bad state!", e);
assert false;
}
return false;
}
private void recycleEvent(MotionEvent event) {
event.recycle();
}
private boolean sendMotionEventAsGesture(
int type, MotionEvent event, Bundle extraParams) {
return sendGesture(type, event.getEventTime(),
(int) event.getX(), (int) event.getY(), extraParams);
}
private boolean sendGesture(
int type, long timeMs, int x, int y, Bundle extraParams) {
assert timeMs != 0;
// The only valid gestures that can occur after the touch sequence has
// ended are SHOW_PRESS and SINGLE_TAP_CONFIRMED, potentially triggered
// after the double-tap delay window times out.
if (mCurrentDownEvent == null
&& type != GestureEventType.SINGLE_TAP_CONFIRMED
&& type != GestureEventType.SHOW_PRESS) {
return false;
}
return mMotionEventDelegate.onGestureEventCreated(type, timeMs, x, y, extraParams);
}
private boolean sendTapEndingEventAsGesture(int type, MotionEvent e, Bundle extraParams) {
if (!sendMotionEventAsGesture(type, e, extraParams)) return false;
mNeedsTapEndingEvent = false;
return true;
}
private void sendTapCancelIfNecessary(MotionEvent e) {
if (!mNeedsTapEndingEvent) return;
if (!sendTapEndingEventAsGesture(GestureEventType.TAP_CANCEL, e, null)) return;
mLastLongPressEvent = null;
}
/**
* @return Whether the ContentViewGestureHandler can handle a MotionEvent right now. True only
* if it's the start of a new stream (ACTION_DOWN), or a continuation of the current stream.
*/
private boolean canHandle(MotionEvent ev) {
return ev.getAction() == MotionEvent.ACTION_DOWN ||
(mCurrentDownEvent != null && mCurrentDownEvent.getDownTime() == ev.getDownTime());
}
/**
* @return Whether the event can trigger a LONG_TAP gesture. True when it can and the event
* will be consumed.
*/
private boolean triggerLongTapIfNeeded(MotionEvent ev) {
if (mLastLongPressEvent != null
&& ev.getAction() == MotionEvent.ACTION_UP
&& !isScaleGestureDetectionInProgress()) {
sendTapCancelIfNecessary(ev);
sendMotionEventAsGesture(GestureEventType.LONG_TAP, ev, null);
return true;
}
return false;
}
/**
* This is for testing only.
* Sends a show pressed state gesture through mListener. This should always be called after
* a down event;
*/
void sendShowPressedStateGestureForTesting() {
if (mCurrentDownEvent == null) return;
mListener.onShowPress(mCurrentDownEvent);
}
/**
* This is for testing only.
* @return Whether a sent TapDown event has been accompanied by a tap-ending event.
*/
boolean needsTapEndingEventForTesting() {
return mNeedsTapEndingEvent;
}
/**
* Update whether multi-touch gestures are supported.
*/
public void updateMultiTouchSupport(boolean supportsMultiTouchZoom) {
mMultiTouchListener.setPermanentlyIgnoreDetectorEvents(!supportsMultiTouchZoom);
}
/**
* Update whether double-tap gestures are supported. This allows
* double-tap gesture suppression independent of whether or not the page's
* viewport and scale would normally prevent double-tap.
* Note: This should never be called while a double-tap gesture is in progress.
* @param supportDoubleTap Whether double-tap gestures are supported.
*/
public void updateDoubleTapSupport(boolean supportDoubleTap) {
assert !isDoubleTapActive();
int doubleTapMode = supportDoubleTap ?
DOUBLE_TAP_MODE_NONE : DOUBLE_TAP_MODE_DISABLED;
if (mDoubleTapMode == doubleTapMode) return;
mDoubleTapMode = doubleTapMode;
updateDoubleTapListener();
}
/**
* Update whether double-tap gesture detection should be suppressed due to
* the viewport or scale of the current page. Suppressing double-tap gesture
* detection allows for rapid and responsive single-tap gestures.
* @param shouldDisableDoubleTap Whether double-tap should be suppressed.
*/
public void updateShouldDisableDoubleTap(boolean shouldDisableDoubleTap) {
if (mShouldDisableDoubleTap == shouldDisableDoubleTap) return;
mShouldDisableDoubleTap = shouldDisableDoubleTap;
updateDoubleTapListener();
}
/**
* @return Whether double-tap gesture detection is enabled.
*/
public boolean isDoubleTapDisabled() {
return mDoubleTapMode == DOUBLE_TAP_MODE_DISABLED || mShouldDisableDoubleTap;
}
/**
* @return Whether the click delay preceding a double tap is disabled.
*/
public boolean isClickDelayDisabled() {
return mDisableClickDelay;
}
/**
* @return Whether a double tap-gesture is in-progress.
*/
public boolean isDoubleTapActive() {
return mDoubleTapMode != DOUBLE_TAP_MODE_DISABLED &&
mDoubleTapMode != DOUBLE_TAP_MODE_NONE;
}
private void updateDoubleTapListener() {
if (isDoubleTapDisabled()) {
// Defer nulling the DoubleTapListener until the double tap gesture is complete.
if (isDoubleTapActive()) return;
mGestureDetector.setOnDoubleTapListener(null);
} else {
mGestureDetector.setOnDoubleTapListener(mDoubleTapListener);
}
}
private boolean isDistanceGreaterThanTouchSlop(float distanceX, float distanceY) {
return distanceX * distanceX + distanceY * distanceY > mScaledTouchSlopSquare;
}
}